package ru.cargobroker.maip.base.util;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.orm.jpa.persistenceunit.MutablePersistenceUnitInfo;
import org.springframework.orm.jpa.persistenceunit.PersistenceUnitPostProcessor;
import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils;
import org.springframework.util.xml.SimpleSaxErrorHandler;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;

/**
 * Post processes persistence unit and appends entity class names and mapping
 * files to the Spring processed persistence unit. Entity names are taken from
 * related PU on the classpath.
 * 
 * @author raimondas
 * @author mmalinauskas
 * @since 1.0
 * 
 */
public class IntegrationTestRealmPersistenceUnitPostProcessor implements PersistenceUnitPostProcessor {
	
	private final Log logger = LogFactory.getLog(IntegrationTestRealmPersistenceUnitPostProcessor.class);
	private final String PU_LOC = "META-INF/persistence.xml";
	private final String JBPM_TYPES = "org/jbpm/db/hibernate.queries.hbm.xml";
	
	/**
	 * Database type used for application execution. If database type is
	 * specified then "META-INF/{databaseType}-persistence.xml" persistence
	 * files are included in persistence unit building.
	 */
	private String databaseType = null;

	/**
	 * Hack for setting default CacheProviderClass this is for iTests only
	 * 
	 * @param properties
	 */
	
	protected void hibernateCacheProviderHook(Properties properties) {
		if (!properties.containsKey("hibernate.cache.provider_class")) {
			if (!"false".equals(properties.getProperty("hibernate.cache.use_second_level_cache"))) {
				logger.info("Setting 'hibernate.cache.provider_class' to 'org.hibernate.cache.HashtableCacheProvider'");
				properties.put("hibernate.cache.provider_class", "org.hibernate.cache.HashtableCacheProvider");
			}
		}
	}
	
	public void postProcessPersistenceUnitInfo(MutablePersistenceUnitInfo pui) {
		
		hibernateCacheProviderHook(pui.getProperties());
		
		URL rootUrl = pui.getPersistenceUnitRootUrl();
		Enumeration<URL> files;
		try {
			files = pui.getClassLoader().getResources(PU_LOC);
		} catch (IOException e) {
			throw new RuntimeException("Failed to load persistence.xml files", e);
		}
		String sUrl = rootUrl.toString();
		sUrl = sUrl.endsWith("/") ? sUrl.concat(PU_LOC) : sUrl.concat("/" + PU_LOC);
		URL rootPUUrl;
		try {
			rootPUUrl = new URL(sUrl);
		} catch (MalformedURLException e) {
			throw new RuntimeException("Failed to create root PU file URL from " + sUrl, e);
		}
		ErrorHandler handler = new SimpleSaxErrorHandler(logger);
		List<URL> processed = new ArrayList<URL>();
		processPersistenceFiles(pui, files, rootPUUrl, processed, handler);
//		if (!org.apache.commons.lang.StringUtils.isBlank(databaseType)) {
//			// process database specific persistence.xml files
//			String dbSpecificPersistenceXMLFileName = databaseType.trim().toLowerCase() + "-persistence.xml";
//			try {
//				files = pui.getClassLoader().getResources("META-INF/" + dbSpecificPersistenceXMLFileName);
//			} catch (IOException e) {
//				throw new RuntimeException("Failed to load " + dbSpecificPersistenceXMLFileName + " files", e);
//			}
//			processPersistenceFiles(pui, files, rootPUUrl, processed, handler);
//		}
	}

	private void processPersistenceFiles(MutablePersistenceUnitInfo pui, Enumeration<URL> files, URL rootPUUrl,
			List<URL> processed, ErrorHandler handler) {
		while (files.hasMoreElements()) {
			URL url = files.nextElement();
			if(rootPUUrl.equals(url) || processed.contains(url))
				continue;
			Document document;
			try {
				document = createDocument(handler, url.openStream());
			} catch (Exception e) {
				throw new RuntimeException("Failed to parse file "+url.toString(), e);
			} 
			parsePersistenceUnit(document.getDocumentElement(), pui);
			processed.add(url);
		}
	}
	
	protected void parsePersistenceUnit(Element persistence, MutablePersistenceUnitInfo pui) {
		List<Element> units = (List<Element>) DomUtils.getChildElementsByTagName(persistence, "persistence-unit");
		Set<String> classes = new HashSet<String>();
		Set<String> mFiles = new HashSet<String>();
		for(Element unit : units) {
			classes.addAll(parseClass(unit));
			mFiles.addAll(parseMappingFiles(unit));
		}
		for(String clazz : classes) {
			if(!pui.getManagedClassNames().contains(clazz))
				pui.addManagedClassName(clazz);
		}
		for(String mFile : mFiles) {
			if(!pui.getMappingFileNames().contains(mFile)) {
				pui.addMappingFileName(mFile);
			}
		}
		List mappings = pui.getMappingFileNames();
		Collections.sort(mappings, new Comparator() {
			public int compare(Object o1, Object o2) {
				String s1 = (String)o1;
				String s2 = (String)o2;
				return s1.equals(JBPM_TYPES) ? -1 : (s2.equals(JBPM_TYPES) ? 1 : s1.compareTo(s2));
			}
		});
	}
	
	protected Set<String> parseClass(Element persistenceUnit) {
		List<Element> classElements = DomUtils.getChildElementsByTagName(persistenceUnit, "class");
		Set<String> classes = new HashSet<String>();
		for (Element element : classElements) {
			String value = DomUtils.getTextValue(element).trim();
			if(StringUtils.hasText(value))
				classes.add(value);
		}
		return classes;
	}

	protected Set<String> parseMappingFiles(Element persistenceUnit) {
		List<Element> files = DomUtils.getChildElementsByTagName(persistenceUnit, "mapping-file");
		Set<String> mFiles = new HashSet<String>();
		for (Element element : files) {
			String value = DomUtils.getTextValue(element).trim();
			if(StringUtils.hasText(value)) {
				mFiles.add(value);
			}
		}
		return mFiles;
	}
	
	protected Document createDocument(ErrorHandler handler, InputStream stream)
		throws ParserConfigurationException, SAXException, IOException {

		DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
		dbf.setNamespaceAware(true);
		DocumentBuilder parser = dbf.newDocumentBuilder();
		parser.setErrorHandler(handler);
		return parser.parse(stream);
	}

	public void setDatabaseType(String databaseType) {
		this.databaseType = databaseType;
	}
}